import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.registry.LocateRegistry;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.io.File;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.BufferedWriter;
import java.io.FileWriter;
import org.farng.mp3.*;
import org.farng.mp3.id3.*;

public class nodo extends UnicastRemoteObject
                      implements NodoInterface
{
    /** IP addresses of all the known hosts */
    ArrayList<String> knownHosts;

    /** Library of all the tracks presented by this host */
    ArrayList<Track> library;

    /** Port to which the node will be exposed */
    int port;

    /** Name given to the node */
    String nodeName;

    /**
     * Main method used to create the node and expose it in the rmiregistry
     */
    public static void main(String args[])
        throws HostException
    {

        String options[] = {null, null, null, null};
        int port;
        ArrayList<Track> library;
        ArrayList<String> knownHosts;
        nodo node;
        BufferedWriter writer;


        for (int i = 0; i < args.length; ++ i){
            if ("-p".equals(args[i])){
                options[0] = args[++ i];
            }else if ("-c".equals(args[i])){
                options[1] = args[++ i];
            }else if ("-b".equals(args[i])){
                options[2] = args[++ i];
            }else if ("-i".equals(args[i])){
                options[3] = args[++ i];
            }else{
                throw new HostException("Uso: nodo -p <puerto> -c <conocidos> -b <biblioteca> -i <ID del nodo>");
            }
        }

        //Options are kept in an array to ensure each one has been specified
        if (null == options[0] || null == options[1] || null == options[2] || null == options[3]){
            throw new HostException("Uso: nodo -p <puerto> -c <conocidos> -b <biblioteca> -i <ID del nodo>");
        }

        try{
            port = Integer.parseInt(options[0]);
        }catch(NumberFormatException e){
            throw new HostException("Puerto '" + options[0] + "' inválido: " + e.getMessage());
        }

        try{
            knownHosts = getKnownHosts(options[1]);
        }catch(HostException e){
            throw new HostException("Error cargando el archivo de hosts conocidos '" + options[1] + "': " + e.getMessage());
        }

        try{
            library = getLibrary(options[2], options[3]);
        }catch(HostException e){
            throw new HostException("Error cargando el archivo de librería '" + options[2] + "': " + e.getMessage());
        }

        //After having the library, write the xml document
        try{
            writer = new BufferedWriter(new FileWriter("playlist.xspf"));

            writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
            writer.write("<playlist>\n");
            writer.write("  <tracklist>\n");
            for(Track t : library){
                writer.write("    <track>\n");
                writer.write("      <location>" + t.getLocation() + "</location>\n");
                writer.write("      <title>" + t.getTitle() + "</title>\n");
                writer.write("      <creator>" + t.getCreator() + "</creator>\n");
                writer.write("      <album>" + t.getAlbum() + "</album>\n");
                writer.write("      <year>" + t.getYear() + "</year>\n");
                writer.write("      <genre>" + t.getGenre() + "</genre>\n");
                writer.write("      <lyric>" + t.getLyric() + "</lyric>\n");
                writer.write("      <composer>" + t.getComposer() + "</composer>\n");
                writer.write("    </track>\n");
            }
            writer.write("  </tracklist>\n");
            writer.write("</playlist>\n");

            writer.close();
        }catch(IOException e){
            throw new HostException("Error al escribir el archivo XSPF");
        }
        try{
            node = new nodo(knownHosts, library, port, options[3]);
        }catch(RemoteException re){
            throw new HostException("Error al crear el nodo para exponer");
        }

        //Create the instance of the rmiregistry
        try{
            LocateRegistry.createRegistry(port);
            //Bind the 'nodo' object to the rmiregistry
            Naming.rebind("rmi://localhost:" + port + "/NodeService", node);
        }catch(RemoteException re){
            throw new HostException("No se pudo crear rmiregistry: " + re.getMessage());
        }catch(MalformedURLException mue){
            throw new HostException("Nombre para ligar a rmi incorrecto: " + mue.getMessage());
        }
        System.out.println("Nodo iniciado con éxito");
  }

    /**
    * Given the file name of a file of known hosts, load them
    * It is expected each line of this file contains a hostname/ip
    * 
    * @param knownHostsFileName known hosts file name to load
    */
    private static ArrayList<String> getKnownHosts(String knownHostsFileName)
        throws HostException
    {
        ArrayList<String> knownHosts = new ArrayList<String>();
        BufferedReader hosts;
        try{
            hosts = new BufferedReader(new FileReader(knownHostsFileName));
        }catch(FileNotFoundException e){
            throw new HostException("El archivo no existe: " + e.getMessage());
        }

        while (true){
            String hostLine;
            try{
                hostLine = hosts.readLine();
            }catch(IOException e){
                throw new HostException("Error de lectura: " + e.getMessage());
            }

            if (null == hostLine){
                break;
            }

            knownHosts.add(hostLine);
        }

        return knownHosts;
    }

    /**
     * 
     */
    private static ArrayList<Track> getLibrary(String sourceDir, String hostId)
        throws HostException
    {
        File directory;
        ArrayList<String> dirToCheck = new ArrayList<String>();
        ArrayList<Track> lib = new ArrayList<Track>();
        Track added;
        MP3File song;
        AbstractID3v2 tag;

        //Parse the sourceDir so it will always end with '/'
        if(sourceDir.charAt(sourceDir.length() - 1) != '/')
            sourceDir += "/";

        directory = new File(sourceDir);
        //Verify that the file given is a directory
        if(!directory.isDirectory())
            throw new HostException("La dirección dada no corresponde a un directorio");

        //Build this directories library and put the recursive directories on hold
        for(File f : directory.listFiles()){
            //If it's a directory, add to the list and continue
            if(f.isDirectory()){
                dirToCheck.add(f.getName());
                continue;
            }
            
            //If it's a file, create a track and add it to the library
            try{
                song = new MP3File(f);
                tag = song.getID3v2Tag();
                added = new Track(sourceDir + f.getName(), tag.getSongTitle(), tag.getLeadArtist(), 
                                  tag.getAlbumTitle(), tag.getYearReleased(), tag.getSongGenre(),
                                  tag.getSongLyric(), tag.getAuthorComposer(), hostId);
                lib.add(added);            
            }catch(IOException ioe){
                throw new HostException("No se pudo leer el archivo mp3 " + f.getName() + ": " + ioe.getMessage());
            }catch(TrackException te){
                throw new HostException("No se pudo crear el track: " + te.getMessage());
            }catch(TagException tage){
                throw new HostException("Error al crear el tag IDv2: " + tage.getMessage());
            }
        }

        //Build the rest of the library recursively
        for(String dir : dirToCheck)
            for(Track t : getLibrary(sourceDir + dir, hostId))
                lib.add(t);

        return lib;
    }

    /**
     * Main constructor for the class
     */
    public nodo(ArrayList<String> kh, ArrayList<Track> lib, int port, String nodeName)
        throws RemoteException
    {
        this.knownHosts = kh;
        this.library = lib;
        this.port = port;
        this.nodeName = nodeName;
    }

    /** Implementation of the consult method specified in the Nodo interface */
    public ArrayList<Track> consult(Request req)
        throws RemoteException, HostException
    {
        NodoInterface host;
        ArrayList<Track> response;
        ArrayList<Track> consult = new ArrayList<Track>();
        ArrayList<String> visited = req.getVisited();

        //If this node was already visited, then return without doing anything else
        for (String visitedNodeName : visited)
            if (visitedNodeName.equals(this.nodeName))
                return consult;

        visited.add(this.nodeName);

        req.setVisited(visited);
        for (String remoteHostName : knownHosts){

            try{
                host = (NodoInterface) Naming.lookup("rmi://" + remoteHostName + ":" + this.port + "/NodeService");
            }catch(NotBoundException nbe){
                throw new HostException("Servicio solicitado no válido: " + nbe.getMessage());
            }catch(MalformedURLException mue){
                throw new HostException("URL mal formada: " + mue.getMessage());
            }

            req.setServerName(remoteHostName);
            //Request additional tracks to this node
            response = host.consult(req);

            //Join results
            for (Track t : response){
                //Check if this song is already in the result set
                boolean found = false;
                for (Track te : consult){
                    if (t.equals(te)){
                        found = true;
                        break;
                    }
                }
                //Song already in the result set
                if (found){
                    continue;
                }
                consult.add(t);
            }
        }

        //Add local results
        for (Track t : library){
            if (-1 != t.getCreator().toLowerCase().indexOf(req.getConsult().getCreator())
            && -1 != t.getTitle().toLowerCase().indexOf(req.getConsult().getTitle())
            && -1 != t.getAlbum().toLowerCase().indexOf(req.getConsult().getAlbum())
            && -1 != t.getYear().toLowerCase().indexOf(req.getConsult().getYear())
            && -1 != t.getGenre().toLowerCase().indexOf(req.getConsult().getGenre())
            && -1 != t.getLyric().toLowerCase().indexOf(req.getConsult().getLyric())
            && -1 != t.getComposer().toLowerCase().indexOf(req.getConsult().getComposer())){
                //Check that the track is not already in the consult
                boolean found = false;
                for (Track te : consult){
                    if (t.equals(te)){
                        found = true;
                        break;
                    }
                }

                if(found)
                    continue;
                //Each client will reach us with some ip. Return the ip on each consult
                t.setServerName(req.getServerName());
                consult.add(t);
            }
        }
        return consult;
    }
    
    /** Implementation of the download method specified in the Nodo interface */
    public File download(Request req)
        throws RemoteException
    {
        return new File(req.getLocation());
    }

    /** Implementation of the getReachables method specified in the Nodo interface */
    public ArrayList<String> getReachables(ArrayList<String> nodes)
        throws RemoteException, HostException
    {
        NodoInterface h;
        ArrayList<String> response;

        //If this node was already visited, then return without doing anything else
        for (String visitedNodeName : nodes)
            if (visitedNodeName.equals(this.nodeName))
                return new ArrayList<String>();

        //Right know this is a known host
        nodes.add(nodeName);
        for (String  remoteHostName : this.knownHosts){
            try{
                h = (NodoInterface) Naming.lookup("rmi://" + remoteHostName + ":" + this.port + "/NodeService");
            }catch(NotBoundException nbe){
                throw new HostException("Servicio solicitado no válido: " + nbe.getMessage());
            }catch(MalformedURLException mue){
                throw new HostException("URL mal formada: " + mue.getMessage());
            }


            //Request additional reachable nodes to this node
            response = h.getReachables(nodes);

            //Join lists
            for (String newNodeName : response){
                boolean inList = false;
                for (String knownNodeName : nodes){
                    if (newNodeName.equals(knownNodeName)){
                        inList = true;
                        break;
                    }
                }
                if (inList)
                    continue;

                nodes.add(newNodeName);
            }
        }
        return nodes;
    }
}